{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Project 1 - Transactions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our code so far:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import itertools\n",
    "import numbers\n",
    "from datetime import timedelta, datetime\n",
    "from collections import namedtuple\n",
    "\n",
    "\n",
    "class TimeZone:\n",
    "    def __init__(self, name, offset_hours, offset_minutes):\n",
    "        if name is None or len(str(name).strip()) == 0:\n",
    "            raise ValueError('Timezone name cannot be empty.')\n",
    "            \n",
    "        self._name = str(name).strip()\n",
    "        # technically we should check that offset is a\n",
    "        if not isinstance(offset_hours, numbers.Integral):\n",
    "            raise ValueError('Hour offset must be an integer.')\n",
    "        \n",
    "        if not isinstance(offset_minutes, numbers.Integral):\n",
    "            raise ValueError('Minutes offset must be an integer.')\n",
    "            \n",
    "        if offset_minutes < -59 or offset_minutes > 59:\n",
    "            raise ValueError('Minutes offset must between -59 and 59 (inclusive).')\n",
    "            \n",
    "        # for time delta sign of minutes will be set to sign of hours\n",
    "        offset = timedelta(hours=offset_hours, minutes=offset_minutes)\n",
    "\n",
    "        # offsets are technically bounded between -12:00 and 14:00\n",
    "        # see: https://en.wikipedia.org/wiki/List_of_UTC_time_offsets\n",
    "        if offset < timedelta(hours=-12, minutes=0) or offset > timedelta(hours=14, minutes=0):\n",
    "            raise ValueError('Offset must be between -12:00 and +14:00.')\n",
    "            \n",
    "        self._offset_hours = offset_hours\n",
    "        self._offset_minutes = offset_minutes\n",
    "        self._offset = offset\n",
    "        \n",
    "    @property\n",
    "    def offset(self):\n",
    "        return self._offset\n",
    "    \n",
    "    @property\n",
    "    def name(self):\n",
    "        return self._name\n",
    "    \n",
    "    def __eq__(self, other):\n",
    "        return (isinstance(other, TimeZone) and \n",
    "                self.name == other.name and \n",
    "                self._offset_hours == other._offset_hours and\n",
    "                self._offset_minutes == other._offset_minutes)\n",
    "    def __repr__(self):\n",
    "        return (f\"TimeZone(name='{self.name}', \"\n",
    "                f\"offset_hours={self._offset_hours}, \"\n",
    "                f\"offset_minutes={self._offset_minutes})\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Account:\n",
    "    transaction_counter = itertools.count(100)\n",
    "    _interest_rate = 0.5  # percentage\n",
    "    \n",
    "    _transaction_codes = {\n",
    "        'deposit': 'D',\n",
    "        'withdraw': 'W',\n",
    "        'interest': 'I',\n",
    "        'rejected': 'X'\n",
    "    }\n",
    "    \n",
    "    def __init__(self, account_number, first_name, last_name, timezone=None, initial_balance=0):\n",
    "        # in practice we probably would want to add checks to make sure these values are valid / non-empty\n",
    "        self._account_number = account_number\n",
    "        self.first_name = first_name\n",
    "        self.last_name = last_name\n",
    "        \n",
    "        if timezone is None:\n",
    "            timezone = TimeZone('UTC', 0, 0)\n",
    "        self.timezone = timezone\n",
    "        \n",
    "        self._balance = float(initial_balance)  # force use of floats here, but maybe Decimal would be better\n",
    "        \n",
    "    @property\n",
    "    def account_number(self):\n",
    "        return self._account_number\n",
    "    \n",
    "    @property \n",
    "    def first_name(self):\n",
    "        return self._first_name\n",
    "    \n",
    "    @first_name.setter\n",
    "    def first_name(self, value):\n",
    "        self.validate_and_set_name('_first_name', value, 'First Name')\n",
    "        \n",
    "    @property\n",
    "    def last_name(self):\n",
    "        return self._last_name\n",
    "    \n",
    "    @last_name.setter\n",
    "    def last_name(self, value):\n",
    "        self.validate_and_set_name('_last_name', value, 'Last Name')\n",
    "        \n",
    "    # also going to create a full_name computed property, for ease of use\n",
    "    @property\n",
    "    def full_name(self):\n",
    "        return f'{self.first_name} {self.last_name}'\n",
    "        \n",
    "    @property\n",
    "    def timezone(self):\n",
    "        return self._timezone\n",
    "    \n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "    \n",
    "    @timezone.setter\n",
    "    def timezone(self, value):\n",
    "        if not isinstance(value, TimeZone):\n",
    "            raise ValueError('Time zone must be a valid TimeZone object.')\n",
    "        self._timezone = value\n",
    "          \n",
    "    @classmethod\n",
    "    def get_interest_rate(cls):\n",
    "        return cls._interest_rate\n",
    "    \n",
    "    @classmethod\n",
    "    def set_interest_rate(cls, value):\n",
    "        if not isinstance(value, numbers.Real):\n",
    "            raise ValueError('Interest rate must be a real number')\n",
    "        if value < 0:\n",
    "            raise ValueError('Interest rate cannot be negative.')\n",
    "        cls._interest_rate = value\n",
    "        \n",
    "    def validate_and_set_name(self, property_name, value, field_title):\n",
    "        if value is None or len(str(value).strip()) == 0:\n",
    "            raise ValueError(f'{field_title} cannot be empty.')\n",
    "        setattr(self, property_name, value)\n",
    "        \n",
    "    def generate_confirmation_code(self, transaction_code):\n",
    "        # main difficulty here is to generate the current time in UTC using this formatting:\n",
    "        # YYYYMMDDHHMMSS\n",
    "        dt_str = datetime.utcnow().strftime('%Y%m%d%H%M%S')\n",
    "        return f'{transaction_code}-{self.account_number}-{dt_str}-{next(Account.transaction_counter)}'\n",
    "    \n",
    "    @staticmethod\n",
    "    def parse_confirmation_code(confirmation_code, preferred_time_zone=None):\n",
    "        # dummy-A100-20190325224918-101\n",
    "        parts = confirmation_code.split('-')\n",
    "        if len(parts) != 4:\n",
    "            # really simplistic validation here - would need something better\n",
    "            raise ValueError('Invalid confirmation code')\n",
    "        \n",
    "        # unpack into separate variables\n",
    "        transaction_code, account_number, raw_dt_utc, transaction_id = parts\n",
    "        \n",
    "        # need to convert raw_dt_utc into a proper datetime object\n",
    "        try:\n",
    "            dt_utc = datetime.strptime(raw_dt_utc, '%Y%m%d%H%M%S')\n",
    "        except ValueError as ex:\n",
    "            # again, probably need better error handling here\n",
    "            raise ValueError('Invalid transaction datetime') from ex\n",
    "          \n",
    "        if preferred_time_zone is None:\n",
    "            preferred_time_zone = TimeZone('UTC', 0, 0)\n",
    "            \n",
    "        if not isinstance(preferred_time_zone, TimeZone):\n",
    "            raise ValueError('Invalid TimeZone specified.')\n",
    "            \n",
    "        dt_preferred = dt_utc + preferred_time_zone.offset\n",
    "        dt_preferred_str = f\"{dt_preferred.strftime('%Y-%m-%d %H:%M:%S')} ({preferred_time_zone.name})\"\n",
    "        \n",
    "        return Confirmation(account_number, transaction_code, transaction_id, dt_utc.isoformat(), dt_preferred_str)\n",
    "    \n",
    "    def make_transaction(self):\n",
    "        return self.generate_confirmation_code('dummy')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now it's time to add in support for the various transaction methods:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Account:\n",
    "    transaction_counter = itertools.count(100)\n",
    "    _interest_rate = 0.5  # percentage\n",
    "    \n",
    "    _transaction_codes = {\n",
    "        'deposit': 'D',\n",
    "        'withdraw': 'W',\n",
    "        'interest': 'I',\n",
    "        'rejected': 'X'\n",
    "    }\n",
    "    \n",
    "    def __init__(self, account_number, first_name, last_name, timezone=None, initial_balance=0):\n",
    "        # in practice we probably would want to add checks to make sure these values are valid / non-empty\n",
    "        self._account_number = account_number\n",
    "        self.first_name = first_name\n",
    "        self.last_name = last_name\n",
    "        \n",
    "        if timezone is None:\n",
    "            timezone = TimeZone('UTC', 0, 0)\n",
    "        self.timezone = timezone\n",
    "        \n",
    "        self._balance = float(initial_balance)  # force use of floats here, but maybe Decimal would be better\n",
    "        \n",
    "    @property\n",
    "    def account_number(self):\n",
    "        return self._account_number\n",
    "    \n",
    "    @property \n",
    "    def first_name(self):\n",
    "        return self._first_name\n",
    "    \n",
    "    @first_name.setter\n",
    "    def first_name(self, value):\n",
    "        self.validate_and_set_name('_first_name', value, 'First Name')\n",
    "        \n",
    "    @property\n",
    "    def last_name(self):\n",
    "        return self._last_name\n",
    "    \n",
    "    @last_name.setter\n",
    "    def last_name(self, value):\n",
    "        self.validate_and_set_name('_last_name', value, 'Last Name')\n",
    "        \n",
    "    # also going to create a full_name computed property, for ease of use\n",
    "    @property\n",
    "    def full_name(self):\n",
    "        return f'{self.first_name} {self.last_name}'\n",
    "        \n",
    "    @property\n",
    "    def timezone(self):\n",
    "        return self._timezone\n",
    "    \n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "    \n",
    "    @timezone.setter\n",
    "    def timezone(self, value):\n",
    "        if not isinstance(value, TimeZone):\n",
    "            raise ValueError('Time zone must be a valid TimeZone object.')\n",
    "        self._timezone = value\n",
    "          \n",
    "    @classmethod\n",
    "    def get_interest_rate(cls):\n",
    "        return cls._interest_rate\n",
    "    \n",
    "    @classmethod\n",
    "    def set_interest_rate(cls, value):\n",
    "        if not isinstance(value, numbers.Real):\n",
    "            raise ValueError('Interest rate must be a real number')\n",
    "        if value < 0:\n",
    "            raise ValueError('Interest rate cannot be negative.')\n",
    "        cls._interest_rate = value\n",
    "        \n",
    "    def validate_and_set_name(self, property_name, value, field_title):\n",
    "        if value is None or len(str(value).strip()) == 0:\n",
    "            raise ValueError(f'{field_title} cannot be empty.')\n",
    "        setattr(self, property_name, value)\n",
    "        \n",
    "    def generate_confirmation_code(self, transaction_code):\n",
    "        # main difficulty here is to generate the current time in UTC using this formatting:\n",
    "        # YYYYMMDDHHMMSS\n",
    "        dt_str = datetime.utcnow().strftime('%Y%m%d%H%M%S')\n",
    "        return f'{transaction_code}-{self.account_number}-{dt_str}-{next(Account.transaction_counter)}'\n",
    "    \n",
    "    @staticmethod\n",
    "    def parse_confirmation_code(confirmation_code, preferred_time_zone=None):\n",
    "        # dummy-A100-20190325224918-101\n",
    "        parts = confirmation_code.split('-')\n",
    "        if len(parts) != 4:\n",
    "            # really simplistic validation here - would need something better\n",
    "            raise ValueError('Invalid confirmation code')\n",
    "        \n",
    "        # unpack into separate variables\n",
    "        transaction_code, account_number, raw_dt_utc, transaction_id = parts\n",
    "        \n",
    "        # need to convert raw_dt_utc into a proper datetime object\n",
    "        try:\n",
    "            dt_utc = datetime.strptime(raw_dt_utc, '%Y%m%d%H%M%S')\n",
    "        except ValueError as ex:\n",
    "            # again, probably need better error handling here\n",
    "            raise ValueError('Invalid transaction datetime') from ex\n",
    "          \n",
    "        if preferred_time_zone is None:\n",
    "            preferred_time_zone = TimeZone('UTC', 0, 0)\n",
    "            \n",
    "        if not isinstance(preferred_time_zone, TimeZone):\n",
    "            raise ValueError('Invalid TimeZone specified.')\n",
    "            \n",
    "        dt_preferred = dt_utc + preferred_time_zone.offset\n",
    "        dt_preferred_str = f\"{dt_preferred.strftime('%Y-%m-%d %H:%M:%S')} ({preferred_time_zone.name})\"\n",
    "        \n",
    "        return Confirmation(account_number, transaction_code, transaction_id, dt_utc.isoformat(), dt_preferred_str)\n",
    "    \n",
    "    def deposit(self, value):\n",
    "        if not isinstance(value, numbers.Real):\n",
    "            raise ValueError('Deposit value must be a real number.')\n",
    "        if value <= 0:\n",
    "            raise ValueError('Deposit value must be a positive number.')\n",
    "        \n",
    "        # get transaction code\n",
    "        transaction_code = Account._transaction_codes['deposit']\n",
    "        \n",
    "        # generate a confirmation code\n",
    "        conf_code = self.generate_confirmation_code(transaction_code)\n",
    "        \n",
    "        # make deposit and return conf code\n",
    "        self._balance += value\n",
    "        return conf_code\n",
    "    \n",
    "    def withdraw(self, value):\n",
    "        # hmmm... repetitive code! we'll need to fix this\n",
    "        # TODO: refactor a function to validate a valid positive number\n",
    "        #       and use in __init__, deposit and \n",
    "        \n",
    "        accepted = False\n",
    "        if self.balance - value < 0:\n",
    "            # insufficient funds - we'll reject this transaction\n",
    "            transaction_code = Account._transaction_codes['rejected']\n",
    "        else:\n",
    "            transaction_code = Account._transaction_codes['withdraw']\n",
    "            accepted = True\n",
    "            \n",
    "        conf_code = self.generate_confirmation_code(transaction_code)\n",
    "        \n",
    "        # Doing this here in case there's a problem generating a confirmation code\n",
    "        # - do not want to modify the balance if we cannot generate a transaction code successfully\n",
    "        if accepted:\n",
    "            self._balance -= value\n",
    "            \n",
    "        return conf_code\n",
    "    \n",
    "    def pay_interestt(self):\n",
    "        interest = self.balance * Account.get_interest_rate() / 100\n",
    "        conf_code = self.generate_confirmation_code(Account._transaction_codes['interest'])\n",
    "        self._balance += interest\n",
    "        return conf_code"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = Account('A100', 'Eric', 'Idle', TimeZone('MST', -7, 0), 100.0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "100.0"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a.balance"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'D-A100-20190602232906-100'"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a.deposit(100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "200.0"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a.balance"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'W-A100-20190602232907-101'"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a.withdraw(100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "100.0"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a.balance"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'X-A100-20190602232907-102'"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a.withdraw(1000)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "100.0"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a.balance"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Refactoring"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So now, let's refactor some code. We have to do that real positive number validation. We'll deal with it in the same we dealt with the name validations, except we won't be storing anything - this is going to be a good candidate for a static method.\n",
    "\n",
    "While we're at it, we'll modify our `__init__` method as well to validate the balance. Remember I had stated in the project description that balances should never be negative. So, I'm going to introduce a subtle bug here where the initialization will validate that the initial balance is a real number, but will not validate that it be non-negative. We'll \"catch\" that bug later."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Account:\n",
    "    transaction_counter = itertools.count(100)\n",
    "    _interest_rate = 0.5  # percentage\n",
    "    \n",
    "    _transaction_codes = {\n",
    "        'deposit': 'D',\n",
    "        'withdraw': 'W',\n",
    "        'interest': 'I',\n",
    "        'rejected': 'X'\n",
    "    }\n",
    "    \n",
    "    def __init__(self, account_number, first_name, last_name, timezone=None, initial_balance=0):\n",
    "        # in practice we probably would want to add checks to make sure these values are valid / non-empty\n",
    "        self._account_number = account_number\n",
    "        self.first_name = first_name\n",
    "        self.last_name = last_name\n",
    "        \n",
    "        if timezone is None:\n",
    "            timezone = TimeZone('UTC', 0, 0)\n",
    "        self.timezone = timezone\n",
    "        \n",
    "        self._balance = Account.validate_real_number(initial_balance)\n",
    "        \n",
    "    @property\n",
    "    def account_number(self):\n",
    "        return self._account_number\n",
    "    \n",
    "    @property \n",
    "    def first_name(self):\n",
    "        return self._first_name\n",
    "    \n",
    "    @first_name.setter\n",
    "    def first_name(self, value):\n",
    "        self.validate_and_set_name('_first_name', value, 'First Name')\n",
    "        \n",
    "    @property\n",
    "    def last_name(self):\n",
    "        return self._last_name\n",
    "    \n",
    "    @last_name.setter\n",
    "    def last_name(self, value):\n",
    "        self.validate_and_set_name('_last_name', value, 'Last Name')\n",
    "        \n",
    "    # also going to create a full_name computed property, for ease of use\n",
    "    @property\n",
    "    def full_name(self):\n",
    "        return f'{self.first_name} {self.last_name}'\n",
    "        \n",
    "    @property\n",
    "    def timezone(self):\n",
    "        return self._timezone\n",
    "    \n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "    \n",
    "    @timezone.setter\n",
    "    def timezone(self, value):\n",
    "        if not isinstance(value, TimeZone):\n",
    "            raise ValueError('Time zone must be a valid TimeZone object.')\n",
    "        self._timezone = value\n",
    "          \n",
    "    @classmethod\n",
    "    def get_interest_rate(cls):\n",
    "        return cls._interest_rate\n",
    "    \n",
    "    @classmethod\n",
    "    def set_interest_rate(cls, value):\n",
    "        if not isinstance(value, numbers.Real):\n",
    "            raise ValueError('Interest rate must be a real number')\n",
    "        if value < 0:\n",
    "            raise ValueError('Interest rate cannot be negative.')\n",
    "        cls._interest_rate = value\n",
    "        \n",
    "    def validate_and_set_name(self, property_name, value, field_title):\n",
    "        if value is None or len(str(value).strip()) == 0:\n",
    "            raise ValueError(f'{field_title} cannot be empty.')\n",
    "        setattr(self, property_name, value)\n",
    "        \n",
    "    @staticmethod\n",
    "    def validate_real_number(value, min_value=None):\n",
    "        if not isinstance(value, numbers.Real):\n",
    "            raise ValueError('Value must be a real number.')\n",
    "            \n",
    "        if min_value is not None and value < min_value:\n",
    "            raise ValueError(f'Value must be at least {min_value}')\n",
    "            \n",
    "        # validation passed, return valid value\n",
    "        return value\n",
    "    \n",
    "    def generate_confirmation_code(self, transaction_code):\n",
    "        # main difficulty here is to generate the current time in UTC using this formatting:\n",
    "        # YYYYMMDDHHMMSS\n",
    "        dt_str = datetime.utcnow().strftime('%Y%m%d%H%M%S')\n",
    "        return f'{transaction_code}-{self.account_number}-{dt_str}-{next(Account.transaction_counter)}'\n",
    "    \n",
    "    @staticmethod\n",
    "    def parse_confirmation_code(confirmation_code, preferred_time_zone=None):\n",
    "        # dummy-A100-20190325224918-101\n",
    "        parts = confirmation_code.split('-')\n",
    "        if len(parts) != 4:\n",
    "            # really simplistic validation here - would need something better\n",
    "            raise ValueError('Invalid confirmation code')\n",
    "        \n",
    "        # unpack into separate variables\n",
    "        transaction_code, account_number, raw_dt_utc, transaction_id = parts\n",
    "        \n",
    "        # need to convert raw_dt_utc into a proper datetime object\n",
    "        try:\n",
    "            dt_utc = datetime.strptime(raw_dt_utc, '%Y%m%d%H%M%S')\n",
    "        except ValueError as ex:\n",
    "            # again, probably need better error handling here\n",
    "            raise ValueError('Invalid transaction datetime') from ex\n",
    "          \n",
    "        if preferred_time_zone is None:\n",
    "            preferred_time_zone = TimeZone('UTC', 0, 0)\n",
    "            \n",
    "        if not isinstance(preferred_time_zone, TimeZone):\n",
    "            raise ValueError('Invalid TimeZone specified.')\n",
    "            \n",
    "        dt_preferred = dt_utc + preferred_time_zone.offset\n",
    "        dt_preferred_str = f\"{dt_preferred.strftime('%Y-%m-%d %H:%M:%S')} ({preferred_time_zone.name})\"\n",
    "        \n",
    "        return Confirmation(account_number, transaction_code, transaction_id, dt_utc.isoformat(), dt_preferred_str)\n",
    "    \n",
    "    def deposit(self, value):\n",
    "        value = Account.validate_real_number(value, min_value=0.01)\n",
    "       \n",
    "        # get transaction code\n",
    "        transaction_code = Account._transaction_codes['deposit']\n",
    "        \n",
    "        # generate a confirmation code\n",
    "        conf_code = self.generate_confirmation_code(transaction_code)\n",
    "        \n",
    "        # make deposit and return conf code\n",
    "        self._balance += value\n",
    "        return conf_code\n",
    "    \n",
    "    def withdraw(self, value):\n",
    "        value = Account.validate_real_number(value, min_value=0.01)\n",
    "        accepted = False\n",
    "        if self.balance - value < 0:\n",
    "            # insufficient funds - we'll reject this transaction\n",
    "            transaction_code = Account._transaction_codes['rejected']\n",
    "        else:\n",
    "            transaction_code = Account._transaction_codes['withdraw']\n",
    "            accepted = True\n",
    "            \n",
    "        conf_code = self.generate_confirmation_code(transaction_code)\n",
    "        \n",
    "        # Doing this here in case there's a problem generating a confirmation code\n",
    "        # - do not want to modify the balance if we cannot generate a transaction code successfully\n",
    "        if accepted:\n",
    "            self._balance -= value\n",
    "            \n",
    "        return conf_code\n",
    "    \n",
    "    def pay_interest(self):\n",
    "        interest = self.balance * Account.get_interest_rate() / 100\n",
    "        conf_code = self.generate_confirmation_code(self._transaction_codes['interest'])\n",
    "        self._balance += interest\n",
    "        return conf_code"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We should now be seeing exceptions when using non-real or negative numbers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = Account('A100', 'Eric', 'Idle', initial_balance=100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Value must be at least 0.01\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    a.deposit(-100)\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Value must be a real number.\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    a.withdraw(\"100\")\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So this is where I am going to leave this implementation. In reality, there are a lof of other things we should consider such as additional validations, defining equality for two `Account` instances, and so on.\n",
    "\n",
    "Also, one major piece we have not really done is any kind of testing! We manually tested various pieces as we were coding, but there are multiple problems with this:\n",
    "- the tests are not easily repeatable - every time we change code in our `Account` class we **really** need to re-run all the tests to make sure everything still works as expected and we have not broken anything.\n",
    "- our tests were woefully inadequate - we tested just a few cases, and almost no edge cases.\n",
    "\n",
    "For serious production work, we need to test at least at the following levels:\n",
    "- unit testing - where we test each piece of code in isolation and make sure all code branches are tested, edge cases are tested, etc (coverage)\n",
    "- integration testing - where we test that multiple parts of our code works together correctly. For example, testing that multiple account creation, deposits, interest deposits, withdrawals, confirmation codes, etc work properly as a whole."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
